/************************************************************************
* particles.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "graphics.h"
#include "list.h"

static struct {
	particle_emitter_t *emitters;
	shader_t *shader;
	GLuint vao;
	GLuint vbo[2];
	int ids;
	v3_t c_pos;
} particles_data = {
	NULL,
	NULL,
	0,
	{0,0},
	0
};

static void particle_init(particle_t *p, v3_t *pos, v3_t *speed, float gravity, float life, float rotation, float scale)
{
	float f;
	p->pos.x = pos->x;
	p->pos.y = pos->y;
	p->pos.z = pos->z;

	p->gravity = gravity*50.0;
	p->rotation = rotation;
	f = scale*0.125;
	p->scale = math_rand_rangef(scale-f,scale+f);

	if (life > 0.0) {
		f = life*0.5;
		p->life = math_rand_rangef(life-f,life+f);
	}else{
		p->life = life;
	}

	p->speed.x = math_rand_rangef(speed->x-1.0,speed->x+1.0);
	p->speed.y = math_rand_rangef(speed->y-1.0,speed->y+1.0);
	p->speed.z = math_rand_rangef(speed->z-1.0,speed->z+1.0);

	p->dtime = 0.0;
}

static int particle_create(particle_emitter_t *e, v3_t *pos, v3_t *speed, float gravity, float life, float rotation, float scale)
{
	particle_t *p;

	p = malloc(sizeof(particle_t));
	if (!p)
		return 1;

	particle_init(p,pos,speed,gravity,life,rotation,scale);

	e->particles = list_push(&e->particles,p);

	return 0;
}
/* comparison functions:
 * return 0 to insert
 * return 1 to insert later
 * return -1 to not insert at all
 */
static int particles_sort_cmp(void *e1, void *e2)
{
	float d1;
	float d2;
	particle_t *p1;
	particle_t *p2;

	p1 = e1;
	p2 = e2;

	d1 = math_distance(&particles_data.c_pos,&p1->pos);
	if (d1 < 0)
		d1 *= -1;

	d2 = math_distance(&particles_data.c_pos,&p2->pos);
	if (d2 < 0)
		d2 *= -1;

	/* return 1 if e1 is closer to the camera than e2 */
	if (d1 <= d2)
		return 1;
	return 0;
}

particle_emitter_t *particles_emit(v3_t *pos, v3_t *dir, float life, float particle_life, float gravity, float scale, int frames, int count, material_t *mat)
{
	int i;
	particle_emitter_t *e;

	e = malloc(sizeof(particle_emitter_t));
	if (!e)
		return NULL;

	e->id = ++particles_data.ids;
	e->mat = mat;
	e->pos.x = pos->x;
	e->pos.y = pos->y;
	e->pos.z = pos->z;
	e->dir.x = dir->x;
	e->dir.y = dir->y;
	e->dir.z = dir->z;
	e->count = count;
	e->gravity = gravity;
	e->life = life;
	e->scale = scale;
	e->particle_life = particle_life;
	e->frames = frames;
	e->dtime = 0.0;
	e->particles = NULL;

	particles_data.emitters = list_push(&particles_data.emitters,e);

	for (i=0; i<count; i++) {
		particle_create(e,pos,dir,gravity,particle_life,0.0,scale);
	}

	return e;
}

void particles_render()
{
	particle_emitter_t *e;
	particle_emitter_t *re;
	particle_t *p;
	particle_t *rp;
	float dtime;
	matrix_t m;
	matrix_t view;
	matrix_t *projection;
	camera_t *cam;
	int ec = 0;
	int pc = 0;
	if (!particles_data.emitters)
		return;

	if (!particles_data.vao) {
		GLfloat vertices[12] = {-0.5,0.5, -0.5,-0.5, 0.5,0.5, 0.5,-0.5};
		GLfloat texcoords[8] = {0.0,0.0, 0.0,1.0, 1.0,0.0, 1.0,1.0};
		glGenVertexArrays(1,&particles_data.vao);
		glBindVertexArray(particles_data.vao);
		glGenBuffers(2, particles_data.vbo);
		glBindBuffer(GL_ARRAY_BUFFER, particles_data.vbo[0]);
		glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*8, vertices, GL_STATIC_DRAW);
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0,2,GL_FLOAT,GL_FALSE,0,0);
		glBindBuffer(GL_ARRAY_BUFFER, particles_data.vbo[1]);
		glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat)*8, texcoords, GL_STATIC_DRAW);
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,0,0);
		glBindBuffer(GL_ARRAY_BUFFER, 0);
	}

	/* shader */
	if (!particles_data.shader) {
		particles_data.shader = shader_create("particles");
		shader_attribute(particles_data.shader,0,"position");
		shader_attribute(particles_data.shader,1,"uvs");
	}

	shader_enable(particles_data.shader);

	camera_view_matrix(&view,NULL);
	cam = camera_get();
	particles_data.c_pos.x = cam->x;
	particles_data.c_pos.y = cam->y;
	particles_data.c_pos.z = cam->z;

	projection = render_get_projection_matrix();

	shader_uniform_matrix(particles_data.shader,"projectionMatrix",projection);
	shader_uniform_matrix(particles_data.shader,"viewMatrix",&view);

	glBindVertexArray(particles_data.vao);
	glEnableVertexAttribArray(0);
	glEnableVertexAttribArray(1);

	dtime = client_dtime();

	e = particles_data.emitters;
	while (e) {
		e->dtime += dtime;

		mat_use(e->mat,particles_data.shader);

		e->particles = list_resort_cmp(&e->particles,particles_sort_cmp);

		p = e->particles;
		while (p) {
			p->dtime += dtime;

			/* render it */
			matrix_init(&m);
			m.data[0] = view.data[0];
			m.data[1] = view.data[4];
			m.data[2] = view.data[8];
			m.data[4] = view.data[1];
			m.data[5] = view.data[5];
			m.data[6] = view.data[9];
			m.data[8] = view.data[2];
			m.data[9] = view.data[6];
			m.data[10] = view.data[10];
			matrix_scale(&m,p->scale,p->scale,p->scale);
			matrix_translate_v(&m,&p->pos);
			shader_uniform_matrix(particles_data.shader,"transformationMatrix",&m);
			shader_uniform_float(particles_data.shader,"frames",e->frames);
			shader_uniform_float(particles_data.shader,"dframe",(e->frames*e->frames)/(p->life/p->dtime));

			glDrawArrays(GL_TRIANGLE_STRIP,0,4);
			pc++;

			if (p->life > 0.0 && p->dtime >= p->life) {
				/* kill it if the emitter has expired */
				if (e->life > 0.0 && e->dtime >= e->life) {
					rp = p;
					p = p->next;
					e->particles = list_remove(&e->particles,rp);
					free(rp);
					continue;
				}
				/* otherwise, reinitialise it as a new particle */
				particle_init(p,&e->pos,&e->dir,e->gravity,e->particle_life,0.0,e->scale);
			}else{
				p->pos.x += p->speed.x*dtime;
				p->pos.y += p->speed.y*dtime;
				p->pos.z += p->speed.z*dtime;
				if (p->gravity > 0.0)
					p->speed.y -= p->gravity*dtime;
			}
			p = p->next;
		}

		ec++;

		if (e->life > 0.0 && e->dtime >= e->life && !e->particles) {
			re = e;
			e = e->next;
			particles_data.emitters = list_remove(&particles_data.emitters,re);
			free(re);
			continue;
		}
		e = e->next;
	}

	glDisableVertexAttribArray(0);
	glDisableVertexAttribArray(1);
	glBindVertexArray(0);

	shader_disable(particles_data.shader);
}
